﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace UdpAsyncEcho
{
    sealed class Client : IDisposable
    {
        // *************************************************************** //
        #region [ member fields ]

        private Socket m_socket;
        private SocketAsyncEventArgs m_asyncEvtArgs;
        private IPEndPoint m_svrEndpoint;

        private byte[] m_recvBuffer;

        private IContentProvider m_provider;

        #endregion

        // *************************************************************** //
        #region [ constructor ]

        public Client(IPEndPoint svrEndpoint, IContentProvider provider)
        {
            m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

            m_asyncEvtArgs = new SocketAsyncEventArgs();
            m_asyncEvtArgs.Completed += this.OnIoCompleted;

            m_svrEndpoint = svrEndpoint;

            m_recvBuffer = new byte[1024];

            m_provider = provider;
        }

        #endregion

        // *************************************************************** //
        #region [ destructor ]

        private bool m_disposed;

        ~Client()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (m_disposed)
                return;

            try
            {
                if (disposing)
                {
                    m_socket.Close();
                    m_asyncEvtArgs.Dispose();
                }
            }
            finally
            {
                m_disposed = true;
            }
        }

        #endregion

        // *************************************************************** //
        #region [ public APIs ]

        public void StartSend()
        {
            string msg = m_provider.NextMessage;

            if (msg == null)
                StartReceive();
            else
            {
                byte[] buffer = Encoding.ASCII.GetBytes(msg);
                m_socket.StartSend(m_asyncEvtArgs, m_svrEndpoint, buffer, ProcessSent);
            }
        }

        #endregion

        // *************************************************************** //
        #region [ private helpers ]

        private void StartReceive()
        {
            m_socket.StartReceive(m_asyncEvtArgs, m_svrEndpoint, m_recvBuffer, ProcessReceived);
        }

        private void ProcessReceived(SocketAsyncEventArgs evtargs)
        {
            if (evtargs.SocketError == SocketError.Success)
            {
                string msg = Encoding.ASCII.GetString(evtargs.Buffer, evtargs.Offset, evtargs.BytesTransferred);

                if (m_provider.Receive(msg))
                    StartReceive();
                else
                {
                    Thread.Sleep(1000);
                    StartSend();
                }
            }
            else
            {
                Console.WriteLine("receive error={0}", new SocketException((int)evtargs.SocketError).Message);
                Dispose();
            }
        }

        private void ProcessSent(SocketAsyncEventArgs evtargs)
        {
            if (evtargs.SocketError == SocketError.Success)
            {
                StartSend();
            }
            else
            {
                Console.WriteLine("send error={0}", new SocketException((int)evtargs.SocketError).Message);
                Dispose();
            }
        }

        private void OnIoCompleted(object sender, SocketAsyncEventArgs evtargs)
        {
            switch (evtargs.LastOperation)
            {
                case SocketAsyncOperation.ReceiveFrom:
                    ProcessReceived(evtargs);
                    break;

                case SocketAsyncOperation.SendTo:
                    ProcessSent(evtargs);
                    break;

                default:
                    throw new ArgumentOutOfRangeException("unknown socket operation.");
            }
        }

        #endregion
    }
}
